package jmine.tec.purge;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import jmine.tec.purge.api.PurgeFailedException;
import jmine.tec.purge.api.Purger;
import jmine.tec.purge.domain.PurgeConfig;
import jmine.tec.purge.domain.PurgeRequest;
import jmine.tec.purge.domain.enumx.PurgeStatus;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.rtm.impl.RtmController;
import bancosys.tec.utils.date.Date;
import bancosys.tec.utils.date.Timestamp;

/**
 * Executor de expurgo.
 * 
 * @author Marcio Ribeiro (mmr)
 * @created Aug 8, 2011
 */
public class PurgeExecutor implements BeanFactoryAware {

    private static final int DEFAULT_MAX_WINDOW_SIZE = 1000;

    private BaseDAO<PurgeRequest> reqDao;

    private BaseDAO<PurgeConfig> cfgDao;

    private final ExecutorService threadPool = Executors.newCachedThreadPool();

    private RtmController rtmController;

    private PurgeController controller;

    private BeanFactory bf;

    /**
     * @return valor do campo controller.
     */
    private PurgeController getController() {
        return this.controller;
    }

    /**
     * @return valor do campo rtmController.
     */
    private RtmController getRtmController() {
        return this.rtmController;
    }

    /**
     * Define valor para campo rtmController.
     * 
     * @param rtmController valor para campo rtmController.
     */
    public void setRtmController(RtmController rtmController) {
        this.rtmController = rtmController;
    }

    /**
     * Define valor para campo controller.
     * 
     * @param controller valor para campo controller.
     */
    public void setController(PurgeController controller) {
        this.controller = controller;
    }

    /**
     * Executa expurgo de forma assincrona.
     * 
     * @param cfg configuração de expurgo.
     */
    public void purge(PurgeConfig cfg) {
        this.purge(cfg, true);
    }

    /**
     * Executa expurgo de forma sincrona.
     * 
     * @param cfg configuração de expurgo.
     */
    public void purgeSync(PurgeConfig cfg) {
        this.purge(cfg, false);
    }

    /**
     * Executa expurgo.
     * 
     * @param cfg configuracao de expurgo.
     * @param async <code>true</code> se deve acionar expurgo assincrono, <code>false</code> caso contrario.
     */
    private void purge(final PurgeConfig cfg, boolean async) {
        if (!cfg.isEnabled()) {
            throw new ConfigIsNotEnabledException();
        }

        PurgeRequest r = this.createRequest(cfg);
        int items = r.getTotalItems();
        int maxWindowSize = r.getMaxWindowSize();

        Future<?> starter = this.threadPool.submit(new PurgeTasksStarter(r.getId(), items, maxWindowSize));
        if (!async) {
            try {
                starter.get();
            } catch (Throwable e) {
                throw new PurgeFailedException(PurgeMessages.FAILED.create(), e);
            }
        }
    }

    /**
     * @param cfg configuracao de expurgo.
     * @return cria requisição de expurgo.
     */
    private PurgeRequest createRequest(final PurgeConfig cfg) {
        try {
            return this.getController().executeTransacted(new Callable<PurgeRequest>() {
                public PurgeRequest call() throws Exception {
                    PurgeRequest r = PurgeExecutor.this.reqDao.createBean();
                    r.setCfgName(cfg.getName());
                    r.setStatus(PurgeStatus.RUNNING);
                    r.setMaxDate(new Date().addDays(-cfg.getDaysToKeep()));
                    r.setMaxWindowSize(cfg.getMaxWindowSize());
                    r.setRequestStartTime(new Timestamp());
                    r.setTotalItems(PurgeExecutor.this.countItemsToPurge(cfg));
                    r.setUserName(PurgeExecutor.this.getController().getSecurityService().getCurrentUser());
                    r.getPersister().save();
                    return r;
                }
            });
        } catch (Exception e) {
            throw new PurgeFailedException(PurgeMessages.FAILED.create(), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.getClass().getName();
    }

    /**
     * Expurgador!
     * 
     * @author Marcio Ribeiro (mmr)
     * @created Aug 10, 2011
     */
    private class PurgeTask implements Callable<Void> {
        private final Long reqId;

        /**
         * Construtor.
         * 
         * @param reqId id de requisição de expurgo.
         */
        PurgeTask(long reqId) {
            this.reqId = reqId;
        }

        /**
         * Executa expurgo.
         * 
         * @return null.
         * @throws Exception caso algo de inesperado ocorra ao executar expurgo.
         */
        public Void call() throws Exception {
            PurgeRequest r = PurgeExecutor.this.reqDao.findByPk(this.reqId);
            PurgeConfig cfg = PurgeExecutor.this.cfgDao.findByNaturalKey(r.getCfgName());
            r.incItemsPurged(PurgeExecutor.this.getPurger(cfg).purge(r.getMaxDate(), r.getMaxWindowSize()));
            r.getPersister().save();
            return null;
        }
    }

    /**
     * Executa o expurgo em blocos para evitar timeout de transação e permitir o acompanhamento via tela.
     * 
     * @author Marcio Ribeiro (mmr)
     * @created Aug 10, 2011
     */
    private class PurgeTasksStarter implements Runnable {

        private final long reqId;

        private final int windows;

        /**
         * Construtor.
         * 
         * @param reqId da requisição de expurgo.
         * @param itemsToPurge numero total de itens para expurgar.
         * @param maxWindowSize numero maximo para expurgar em uma mesma transacao.
         */
        PurgeTasksStarter(long reqId, int itemsToPurge, int maxWindowSize) {
            int max = maxWindowSize > 0 ? maxWindowSize : DEFAULT_MAX_WINDOW_SIZE;
            this.reqId = reqId;
            this.windows = Math.max(1, (int) Math.ceil((double) itemsToPurge / max));
        }

        /**
         * Dispara tarefas de expurgo.
         */
        public void run() {
            try {
                for (int w = 0; w < this.windows; ++w) {
                    PurgeExecutor.this.getController().executeTransacted(new PurgeTask(this.reqId));
                }
                this.requestFinished();
            } catch (Exception e) {
                this.requestFailed(e);
                PurgeExecutor.this.getRtmController().monitor(e);
                throw new PurgeFailedException(PurgeMessages.FAILED.create(), e);
            }
        }

        /**
         * Requisição terminou com sucesso. Altera status de requisição para sucesso.
         * 
         * @throws Exception caso algo de inesperado ocorra ao salvar status de sucesso de requisição.
         */
        private void requestFinished() throws Exception {
            PurgeExecutor.this.getController().executeTransacted(new Callable<Void>() {
                public Void call() throws Exception {
                    PurgeRequest r = PurgeExecutor.this.reqDao.findByPk(PurgeTasksStarter.this.reqId);
                    r.setRequestEndTime(new Timestamp());
                    r.setStatus(PurgeStatus.OK);
                    r.getPersister().save();
                    return null;
                }
            });
        }

        /**
         * Requisição de expurgo falhou por algum motivo.
         * 
         * @param cause causa de falha.
         */
        private void requestFailed(final Throwable cause) {
            try {
                PurgeExecutor.this.getController().executeTransacted(new Callable<Void>() {
                    public Void call() throws Exception {
                        PurgeRequest r = PurgeExecutor.this.reqDao.findByPk(PurgeTasksStarter.this.reqId);
                        r.setRequestEndTime(new Timestamp());
                        r.setStatus(PurgeStatus.NOT_OK);
                        r.setErrorMessage(cause.getMessage());
                        r.getPersister().save();
                        return null;
                    }
                });
            } catch (Exception e) {
                PurgeExecutor.this.getRtmController().monitor(e);
                throw new PurgeFailedException(PurgeMessages.FAILED.create(), e);
            }
        }
    }

    /**
     * @param cfg configuracao de expurgo.
     * @return expurgador.
     */
    private Purger getPurger(PurgeConfig cfg) {
        return (Purger) this.bf.getBean(cfg.getPurgerBeanName(), Purger.class);
    }

    /**
     * Define valor para campo reqDao.
     * 
     * @param reqDao valor para campo reqDao.
     */
    public void setReqDao(BaseDAO<PurgeRequest> reqDao) {
        this.reqDao = reqDao;
    }

    /**
     * Define valor para campo cfgDao.
     * 
     * @param cfgDao valor para campo cfgDao.
     */
    public void setCfgDao(BaseDAO<PurgeConfig> cfgDao) {
        this.cfgDao = cfgDao;
    }

    /**
     * {@inheritDoc}
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.bf = beanFactory;
    }

    /**
     * @param cfg configuracao de expurgo.
     * @return número de itens a serem expurgados.
     */
    public int countItemsToPurge(PurgeConfig cfg) {
        return this.getPurger(cfg).countItemsToPurge(new Date().addDays(-cfg.getDaysToKeep()));
    }
}
